
package hasthi.wsdm;

import static hasthi.common.constants.WSDMConstants.RESOURCE_NAME_PARAM;
import hasthi.common.ErrorManager;
import hasthi.common.HasthiException;
import hasthi.common.ManagementAgentException;
import hasthi.common.ManagementNotifier;
import hasthi.common.HasthiException.FaultCode;
import hasthi.common.constants.HasthiConstants;
import hasthi.config.CapabilityType;
import hasthi.config.OperationType;
import hasthi.config.PropertyType;
import hasthi.wsrf.WSResource;
import hasthi.wsrf.WsrfException;
import hasthi.wsrf.faults.DeleteResourcePropertiesRequestFailedFault;
import hasthi.wsrf.faults.InsertResourcePropertiesRequestFailedFault;
import hasthi.wsrf.faults.UpdateResourcePropertiesRequestFailedFault;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlbeans.XmlObject;
import org.w3.x2005.x08.addressing.EndpointReferenceDocument;
import org.w3.x2005.x08.addressing.EndpointReferenceType;

/**
 * This class provides a implementation of the manageable resource and it is extensible via 
 * Adding capabilities. Even though it is possible to have properties that do not bound to a capability, usually they are added via a 
 * capability. Capabilites are added via management-config.xml file and each capability may have properties, operations, and events. 
 * <ol>
 *      <li>Each property must be defined in a capability and value of this managent resource may be defined via managementConfiguration/Resource (if it is static) or it might be defined 
 *      in the implementation of the capability (e.g. Matrics/configuration)</li>
 *      <li>Each operation must be defined with in a capability, and capability implementation must have method that matches the signature of operations</li>
 *      <li>Generating events should be handled by user, however it is recommeded that method for sending events are defined in each capability class</li>
 * </ol>       
 * 
 * For an example following is sample manageable resource which has two capabilities. Custom capabilities can be defined and linked by their capability URL 
 * <Resource>
 *      <resourceId>sr1</resourceId>
 *      <capability>http://docs.oasis-open.org/wsdm/muws/capabilities/Description</capability>
 *      <capability>http://docs.oasis-open.org/wsdm/muws/capabilities/State</capability>
 *      <properties>
 *          <foo1>bar1</foo1>
 *          <foo2>bar2</foo2>
 *     </properties>   
 * </Resource>
 *  
 * @author Srinath Perera(hperera@cs.indiana.edu)
 */
public class ManagableResource implements WSResource{
    protected static Log log = LogFactory.getLog(HasthiConstants.AGENT_LOGGER_NAME);
    private HashMap<QName, Object> resourceProperties = new HashMap<QName, Object>();
    private Map<QName,Capability> qName2CapabilityMap = new HashMap<QName, Capability>();
    private Map<String,Capability> operation2CapabilityMap = new HashMap<String, Capability>();
    private List<Capability> capabilities;
    
    
    private List<ManagementNotifier> notifiers = new Vector<ManagementNotifier>(2);
    private List<ManagementNotifier> notifiers2Remove = new Vector<ManagementNotifier>(2);
    
    private ManagementAgentContext agentContext;
    /**
     * Epr is is Epr representation of resourceEndpoint
     */
    private EndpointReferenceType epr;
    private final ManagedSystemHandle systemHandle;
    private long lastManagmeTime = -1;
    
    public ManagableResource(ManagementAgentContext agentContext,ManagedSystemHandle managedSystemHandle) {
        this.systemHandle = managedSystemHandle;
        QName name = systemHandle.getResourceName();
        resourceProperties.put(RESOURCE_NAME_PARAM,name.getNamespaceURI()+"/"+name.getLocalPart());
        
//      Create new Managable resource
        EndpointReferenceDocument selfEpr = EndpointReferenceDocument.Factory.newInstance();
        epr = selfEpr.addNewEndpointReference();
        epr.addNewAddress().setStringValue(managedSystemHandle.getSystemEndPoint());
        this.agentContext = agentContext;
    }
    
    public void init(List<Capability> capabilities) throws WsrfException{
        this.capabilities = capabilities;
        for(Capability capability:capabilities){
            CapabilityDescription capabilityDescription = capability.getCapabilityDescription();
            CapabilityType capabilityConfig = capabilityDescription.getCapabilityConfig();
            PropertyType[] properties = capabilityConfig.getPropertyArray();
            if(properties != null){
                for(PropertyType property:properties){
                    QName name = property.getName();
                    qName2CapabilityMap.put(name,capability);
                }
            }
            OperationType[] operations = capabilityConfig.getOperationArray();
            if(operations != null){
                for(OperationType operation:operations){
                    operation2CapabilityMap.put(operation.getSoapAction(), capability);
                }
            }
        }
    }


    public QName getName() {
        return systemHandle.getResourceName();
    }

    public HashMap<QName, Object> getResourceProperties()throws WsrfException {
        try {
            HashMap<QName , Object> tempMap =  new HashMap<QName, Object>();
            for(Capability capability:capabilities){
                CapabilityDescription capabilityDescription = capability.getCapabilityDescription();
                CapabilityType capabilityConfig = capabilityDescription.getCapabilityConfig();
                PropertyType[] properties = capabilityConfig.getPropertyArray();
                if(properties != null){
                    for(PropertyType property:properties){
                        QName name = property.getName();
                        Object value = capability.getProperty(name);
                        if(value != null){
                            tempMap.put(name, value);    
                        }else{
                            throw new ManagementAgentException("Capability "+capability.getCapabilityDescription().getCapabilityURI() 
                                    + " defines property "+ name + " but it does not have a value", FaultCode.InitalizationError);
                        }
                        
                    }
                }
            }
            tempMap.put(RESOURCE_NAME_PARAM, systemHandle.getResourceName().getNamespaceURI()
                    + "/"+ systemHandle.getResourceName().getLocalPart());
            return tempMap;
        } catch (HasthiException e) {
            throw new ManagementAgentException(e, e.getFaultCode());
        }
    }

    public Object getResourceProperty(QName name)throws ManagementAgentException {
        try {
            Capability capability = qName2CapabilityMap.get(name);
            if(capability != null){
                return capability.getProperty(name);    
            }else{
                throw new ManagementAgentException("No capability handles the resource property "+ name, FaultCode.InvaliedRequest);
            }
        } catch (HasthiException e) {
            throw new ManagementAgentException(e,e.getFaultCode());
        }
    }

    public void insertResourceProperties(QName name, Object value) throws InsertResourcePropertiesRequestFailedFault {
        if(resourceProperties.containsKey(name) || qName2CapabilityMap.containsKey(name)){
            throw new InsertResourcePropertiesRequestFailedFault(epr,name,"Resource Property Already exisits", FaultCode.InvaliedRequest);
        }
        resourceProperties.put(name, value);
    }


    public void setResourceProperties(HashMap<QName, Object> properties) throws ManagementAgentException {
        try {
            for(QName key:properties.keySet()){
                Capability capability = qName2CapabilityMap.get(key);
                if(capability != null){
                    capability.setProperty(key,properties.get(key));    
                }else{
                    throw new ManagementAgentException("No capability defines the property "+ key,FaultCode.InvaliedRequest);
                }
            }
        } catch (HasthiException e) {
            throw new ManagementAgentException(e,e.getFaultCode());
        }
    }

    public void updateResourceProperties(QName name, Object value) throws UpdateResourcePropertiesRequestFailedFault {
        try {
            Capability capability = qName2CapabilityMap.get(name);
            capability.setProperty(name,value);
        } catch (HasthiException e) {
            throw new UpdateResourcePropertiesRequestFailedFault(epr,name,e,e.getFaultCode());
        }
        
    }
    
    public void deleteResourceProperty(QName name) throws DeleteResourcePropertiesRequestFailedFault {
       if(resourceProperties.containsKey(name)){
           resourceProperties.remove(name);
       }else{
           throw new DeleteResourcePropertiesRequestFailedFault(epr,name,"Resouce Properties of capabilities can not be deleted ", FaultCode.InvaliedRequest);
       }
    }
    public void queryResourceProperties(QName name) {
        throw new UnsupportedOperationException();
    }
    
    /**
     * For now we pass requests as strings to support XSUL and Axis2 both, however we might need to change this later
     * @param requestAsStr
     * @return
     * @throws HasthiException 
     */
    
    public XmlObject dispatchOperation(String action,XmlObject request) throws ManagementAgentException{
        try {
            Capability capability = operation2CapabilityMap.get(action);
            
            OperationType[] operations = capability.getCapabilityDescription().getCapabilityConfig().getOperationArray();
            for(OperationType operation:operations){
                if(operation.getSoapAction().equals(action)){
                    String operationName = operation.getOperationName();
                    Method[] mathods = capability.getClass().getMethods();
                    for(Method method:mathods){
                        if(method.getName().equals(operationName)){
                            XmlObject response = (XmlObject)method.invoke(capability, new Object[]{request});
                            return response;
                        }
                    }
                    throw new ManagementAgentException("Operation "+ operationName + " is not defined in "+ capability.getClass() + " even though it is defined in configuration", FaultCode.InvaliedRequest);
                }
            }
            throw new ManagementAgentException("Operation for action "+ action + " is not found ", FaultCode.InvaliedRequest);
        } catch (SecurityException e) {
            throw new ManagementAgentException(e,FaultCode.InternalServiceError);
        } catch (IllegalArgumentException e) {
            throw new ManagementAgentException(e,FaultCode.InvaliedRequest);
        } catch (IllegalAccessException e) {
            throw new ManagementAgentException(e,FaultCode.InternalServiceError);
        } catch (InvocationTargetException e) {
            throw new ManagementAgentException(e,FaultCode.InternalServiceError);
        }
    }
    
    public String[] getActionsForOperations(){
        return operation2CapabilityMap.keySet().toArray(new String[0]);
    }


    public List<Capability> getCapabilities() {
        return capabilities;
    }

    public synchronized void sendEvent(XmlObject eventData) throws HasthiException{
        int length = notifiers.size();
        
        HasthiException error = null;
        for(int i = 0;i<length;i++){
            ManagementNotifier notifier = notifiers.get(i);
            try {
                notifier.sendEvent(eventData);
            } catch (HasthiException e) {
                if(notifier.sendingEventFalied(e)){
                    notifiers2Remove.add(notifier);
                }
                //We have to do something to stop one failire from sending events to others
                log.warn("Failed while sending event to "+notifier.getName()+ " "+ e.getMessage());
                ErrorManager.error(e);
                error =  e;
            }
        }
        if(notifiers2Remove.size() > 0){
            log.debug("RemoveNotifier:"+ getName() + " "+ notifiers2Remove);
            notifiers.removeAll(notifiers2Remove);
            notifiers2Remove.clear();
        }
        if(notifiers.size() == 0){
            log.warn("No Notifier registered for "+getName());
            //throw new HasthiException("No Notifier registered for "+getName(), FaultCode.LocalError);    
        }
        if(error != null){
            throw error;
        }
    }

    public synchronized void addNotifier(ManagementNotifier notifier) {
        log.debug("AddNotifer:"+ getName() + " "+ notifier.getName());
        notifiers.add(notifier);
    }
    
    public synchronized void removeNotifier(String name) throws HasthiException {
        ManagementNotifier notifier2Remove = null;
        int length = notifiers.size();
        for(int i = 0;i<length;i++){
            ManagementNotifier notifier = notifiers.get(i);
            if(notifier.getName().equals(name)){
                notifier2Remove = notifier;
            }
        }
        if(notifier2Remove != null){
            notifiers.remove(notifier2Remove);    
            log.debug("RemoveNotifier:"+ getName() + " "+ notifier2Remove);
        }
    }
    
    public EndpointReferenceType getResourceEndpointEPR(){
        return epr;
    }

    public String getResourceEndpoint() {
        return systemHandle.getSystemEndPoint();
    }
    
    public void stop() throws HasthiException{
        //stop each capability. This is useful for things like stopping heartbeat capability
        for(Capability capability:capabilities){
            capability.stop();
        }
        
        int length = notifiers.size();
        for(int i = 0;i<length;i++){
            ManagementNotifier notifier = notifiers.get(i);
            notifier.shutDown();
        }    
        agentContext.getUnicastContianier().stopContainer();
    }

    public ManagedSystemHandle getSystemHandle() {
        return systemHandle;
    }

    public long getLastManagmeTime() {
        return lastManagmeTime;
    }

    public void setLastManagmeTime(long lastManagmeTime) {
        this.lastManagmeTime = lastManagmeTime;
    }
}

